package fhq.hcmute.qrcode.decoder;

import fhq.hcmute.qrcode.QRCode;
import fhq.hcmute.qrcode.common.BitMatrix;
import fhq.hcmute.qrcode.common.DataBlock;
import fhq.hcmute.qrcode.common.DataMode;
import fhq.hcmute.qrcode.common.ErrorCorrectionLevel;
import fhq.hcmute.qrcode.common.Version;
import fhq.hcmute.qrcode.encoder.BitArray;
import fhq.hcmute.qrcode.reedsolomon.ReedSolomon;
import fhq.hcmute.qrcode.util.AlphanumericTable;
import fhq.hcmute.qrcode.util.QRCodeException;

/**
 * @author Hien Nguyen Le
 * @Email: nglehien@gmail.com
 */
public class Decoder {

	public QRCode decode(BitMatrix bitMatrix) throws QRCodeException {
		MatrixConverter converter = new MatrixConverter(bitMatrix);

		Version version = converter.getVersion();
		FormatInformation formatInformation = converter.getFormatInformation();
		DataBlock dataBlock = converter.readContents();

		for (DataBlock.Block b : dataBlock.getBlocks()) {
			int[] newValues = errorCorrection(b.getCodewords(), version,
					formatInformation.getCorrectionLevel());
			if (newValues != null) {
				b.setCodewords(newValues);
			}
		}
		int[] dataCodewords = dataBlock.getDataCodewords();
		DataMode dataMode = getDataMode(dataCodewords);
		String contents = decodeData(dataCodewords, version, dataMode);

		QRCode qrCode = new QRCode(formatInformation.getCorrectionLevel(),
				formatInformation.getMaskPattern(), version, dataMode, contents);
		qrCode.setBitMatrix(bitMatrix);
		return qrCode;
	}

	private int[] errorCorrection(int[] dataCodewords, Version version,
			ErrorCorrectionLevel level) throws QRCodeException {
		int n = version.getNSyndromes(level);
		ReedSolomon reedSolomon = new ReedSolomon();
		return reedSolomon.decode(dataCodewords, n);
	}

	private DataMode getDataMode(int[] dataCodewords) {
		BitArray bitArray = new BitArray();
		bitArray.append(8, dataCodewords);
		DataMode mode = DataMode.parse(bitArray.getRange(0, 3).toString());
		return mode;
	}

	public String decodeData(int[] dataCodewords, Version version, DataMode mode) {
		BitArray bitArray = new BitArray();
		bitArray.append(8, dataCodewords);
		int characterCount = mode.getCharacterCount(version);
		int wordLenght = Integer.parseInt(
				bitArray.getRange(4, 4 + characterCount - 1).toString(
						BitArray.NONE_DELIMIT), 2);
		BitArray bitData = bitArray.getRange(4 + characterCount, 4
				+ characterCount + mode.getTotalBitRespective(wordLenght) - 1);

		if (mode.equals(DataMode.ALPHANUMERIC)) {
			return decodeAplanumeric(bitData);
		} else if (mode.equals(DataMode.NUMERIC)) {
			return decodeNumerric(bitData);
		} else {
			return decodeBinary(bitData);
		}
	}

	/**
	 * Decode ALPHANUMERIC mode.
	 * 
	 * @return String
	 */
	private String decodeAplanumeric(BitArray bitArray) {
		int length = bitArray.getSize();
		int count = 0;
		StringBuilder stringBuilder = new StringBuilder();
		for (int i = 0; i < length / 11; i++) {
			String value = bitArray.getRange(count, count + 11 - 1).toString(
					BitArray.NONE_DELIMIT);
			int v = Integer.parseInt(value, 2);
			stringBuilder.append(AlphanumericTable.getCharByValue(v / 45));
			stringBuilder.append(AlphanumericTable.getCharByValue(v % 45));
			count += 11;
		}
		if (length % 11 != 0) {
			String value = bitArray.getRange(count, count + 6 - 1).toString(
					BitArray.NONE_DELIMIT);
			int v = Integer.parseInt(value, 2);
			stringBuilder.append(AlphanumericTable.getCharByValue(v));
		}
		return stringBuilder.toString();
	}

	/**
	 * Decode NUMBERRIC mode.
	 * 
	 * @return String
	 */
	private String decodeNumerric(BitArray bitArray) {
		int length = bitArray.getSize();
		int count = 0;
		StringBuilder stringBuilder = new StringBuilder();
		for (int i = 0; i < length / 10; i++) {
			String value = bitArray.getRange(count, count + 10 - 1).toString(
					BitArray.NONE_DELIMIT);
			int v = Integer.parseInt(value, 2);
			stringBuilder.append(v);
			count += 10;
		}
		if (length % 10 != 0) {
			String value = bitArray.getRange(count, count + length % 10 - 1)
					.toString(BitArray.NONE_DELIMIT);
			int v = Integer.parseInt(value, 2);
			stringBuilder.append(v);
		}
		return stringBuilder.toString();
	}

	/**
	 * Decode BINARY mode.
	 * 
	 * @return String
	 */
	private String decodeBinary(BitArray bitArray) {
		return new String(bitArray.getByteArray());
	}

}
